<?php
//Класс инкапсулирующий работу с объектом, представленным записью в базе данных
class Object {
	
    //Информация о представлении объекта в базе данных
    protected $fields = array();

    //Имя объекта (русское) и оно же во множественном числе
	protected $name, $names;
	
    protected $db_field_prefix, $dbf;
    protected $db_table_name, $tbl;
    
    //id, имя класса объекта и загруженные свойства объекта
    protected $id;
	protected $id_key;
	protected $class_name;	
    protected $properties = array();
    
	//Старые значения свойств объекта, которые иногда нужно знать при апдейте (удалить старые картинки)
	protected $old_row;
	protected $old_loaded = FALSE;
	
    //Состояние объекта
    protected $is_loaded = FALSE;
    protected $is_updated = FALSE;
    protected $is_saved = FALSE;
    /*
    //Дополнительные поля в бд и возможность их включения/выключения
    protected $db_system_fields = array(
        'created'=>array('type'=>'TIMESTAMP', 'null'=>true),
        'updated'=>array('type'=>'TIMESTAMP', 'null'=>true)
    );
    protected $system_fields = array('created'=>false, 'updated'=>false);
    */
    //Вычислимые свойства
    protected $computed_properties = array();
    
    //Присоединённые свойства
    protected $join_properties = array();
    
    //Ошибки, возникающие при работе с объетом  
    protected $errors = array();

    //Объект с заданным id или новый объект, $id_key используется для идентификации объекта
	//по другому полю, например idn
    public function __construct($id = FALSE, $id_key = 'id') {
        $this->id = $id;
		$this->id_key = $id_key;
		
		$this->class_name = get_class($this);
		//Псевдонимы для быстрого доступа
        $this->dbf = $this->db_field_prefix;
        $this->tbl = $this->db_table_name;
        /*
        //Добавляем служебные поля
        foreach ($this->system_fields as $system_field => $flag)
            if ($flag) $this->db_fields[$system_field] = $this->db_system_fields[$system_field];
        */
		
		//Изначально все свойства NULL
        $this->properties = array_fill_keys(array_keys($this->fields), NULL);
    }
	
    //Метод позволяющий создавать таблицу для класса без явного инстанцирования объекта
    public static function createTable($class_name) {
        $obj = new $class_name();
        $obj->_createTable();               
    }
    
    public function _createTable() {        
        DB::query(q::ini($this->tbl)->drop()->end);
		        
        //Поля для сохранения времени создания и изменения должны обрабатываться отдельно
        /*$clear_fields = $this->db_fields;
        $created = false;
        $updated = false;
        if (isset($clear_fields['updated'])) {
            unset($clear_fields['updated']);
            $updated = true;
        }
        if (isset($clear_fields['created'])) {
           unset($clear_fields['created']);
           $created = true;
        }
        */
        DB::query(q::ini($this->tbl, $this->dbf)->create($this->fields)->end);
    }
    
    //При необходимости метод переопределяется в дочерних классах
    //Позволяет вычислять дополнительные свойства объекта на основе определённых в БД
    protected function computeFields() {
        
    }
    
    //Загрузка объекта из базы данных
    private function load() {
        if (!$this->is_loaded && $this->id) {        	
            $query = q::ini($this->tbl, $this->dbf)->select()->where($this->id_key, $this->id)->end;
            $row = DB::selectOne($query);

            if (!$row)
                return $this->errorNotExist();

            $this->properties = $this->convertFieldsFromDB($this->fields, $this->dbf, $row);
            $this->id = $this->properties['id']; 
        }
        
        $this->is_loaded = TRUE;
        $this->computeFields();
        return TRUE;
    }
    
	//До сих пор операции чтения и установки значений не перескались
	//Поэтому для хранения текущих значений свойств объекта использовался
	//только один массив $this->properties, после установки свойст, уже нельзя загрузить исходные
	//они перезапишут их. Но иногда нужно узнать старые значения, например, если нужно удалить картинки
	//для это нужен этот метод. Можно рассматривать его как хак и потом переписать
	protected function fastLoadRow() {
		if (!$this->old_loaded) {
			$query = q::ini($this->tbl, $this->dbf)->select()->where('id', $this->id)->end;
            $this->old_row = DB::selectOne($query);
			$this->old_loaded = TRUE;
		}
		return $this->old_row;		
	}
		
    public function isExist() {
        return $this->load();
    }
    
    //Получение значения одного поля объекта
    public function getField($field_name) {
        $fields = $this->getFields($field_name);
        return $fields[$field_name];
    }
    
    //Получения значения полей объекта
    public function getFields() {
        if (!$this->load()) return false;
		$fields = paramsToArray(func_get_args());

        if (!$fields)
            $fields = array_merge(array_keys($this->fields), array_keys($this->computed_properties), array_keys($this->join_properties));
            
        array_push($fields, 'id');
         
        return array_intersect_key($this->properties+$this->computed_properties+$this->join_properties, array_flip($fields));
    }
    
    //Значение поля, подготовленное для вывода в html
    public function getHTMLField($field_name) {
        $fields = $this->getFields($field_name);
        return htmlspecialchars($fields[$field_name], ENT_QUOTES, 'UTF-8', FALSE);
    }
    
    
    //Возвращает значения полей подготовленный для вывода в html
    public function getHTMLFields($fields = array()) {
        $values = $this->getFields($fields);
        $new_values = array();
        foreach ($values as $key=>$value)
          $new_values[$key] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8', FALSE);
        return $new_values;
    }
    
    function getId() {
        if (!$this->id) $this->save();
        return $this->id;
    }
    
    //Установка значений полей
    //Может вызываться либо с двумя параметрами: ключ, значение
    //Либо с одним: ассоциативным массивом ключей и значений
    public function setFields() {
        if (func_num_args() == 2)
          $fields = array(func_get_arg(0) => func_get_arg(1));
        else
          $fields = func_get_arg(0);
          
       return $this->_setFields($fields, FALSE);
    }
    
    protected function _setFields($fields, $is_safe = FALSE) {
        $this->load();
        //Проверяем, не пытаемся ли установить значения несуществующих полей
        if (!$is_safe) {
            $fields_not_exist = array_diff(array_keys($fields), array_keys($this->properties));
            if ($fields_not_exist) 
                return $this->errorFieldsNotExist($fields_not_exist);
        }
        
        $this->properties = array_merge_override($this->properties, $fields);
          
        $this->is_updated = TRUE;  
        return $this;  
    }
    /*
    //Версия функции для устновки значений из другого объекта. Игнорирует лишние поля
    public function safeSetFields($fields) {
        unset($fields['id']);       
        return $this->_setFields($fields, TRUE);
    }
    */
    //Принудительная установка полей объекта. Используется, когда одним запросом получена информация сразу
    //о многих объектах и нужно суметь пользоваться ими в дальнейшем, так как если бы они сами загрузили
    //информацию о себе 
    public function forcedSetFields($db_row) {
        $this->properties = $this->convertFieldsFromDB($this->fields, $this->dbf, $db_row);
        $this->id = $this->properties['id'];
        $this->computeFields();
        $this->is_loaded = TRUE;
    }
    	
    //Сохранение объекта
    public function save($is_filter = TRUE, $save_files = TRUE) { 
        //Если это новый объект, нужно добавить запись в БД
        if (!$this->id && !$this->is_saved) {
            //Если необходимо сохраняем дату создания
            //if ($this->system_fields['created'])
            //    $this->properties['created'] = date('Y-m-d H:i:s');

            //Проверяем, что установлены все значения, которые не могут NULL
            if (!$this->checkNotNull()) 
                return FALSE;         
            
            if ($save_files)
			    $this->saveFiles();

			$fields = $this->convertFieldsToDB($is_filter);

            $this->id = DB::insert(q::ini($this->tbl, $this->dbf)->insert(array_filter($fields, 'is_scalar'))->end);
            
                        
            $this->properties['id'] = $this->id;
            $this->is_updated = FALSE;
            $this->is_saved = TRUE;         
        }
        //Уже существующий объект, нужно внести изменения
        else if ($this->is_updated) {
            	$fields = $this->convertFieldsToDB($is_filter);
                unset($fields['updated']);
				
				$this->updateFiles();
				
				//Отфильтровываем те поля, которые не должны перезаписываться в том случае, если они
				//не установлены, например, файлы				
				if ($is_filter)
				    $fields = $this->filterFields($fields);
				
                DB::query(q::ini($this->tbl, $this->dbf)->update($fields)->where('id', $this->id)->end);
        }
    }
	
	protected function filterFields($fields) {
		$result = array();
		foreach ($fields as $key=>$value) {
			if (!(($this->fields[$key]['type'] == 'image' || $this->fields[$key]['type'] == 'file') && !$value)) {
				$result[$key] = $value;
			}
		}
		return $result;
	}
    
	protected function saveFiles() {
		foreach ($this->properties as $key=>$value) {
			$type = $this->fields[$key]['type'];
			
			
			if ($type == 'image' && $value) {
				$tmp_path = Path::temp_files($value);
				//Создаём большую и маленькие картинки
				$x_size = $this->fields[$key]['x_size'];
				$y_size = $this->fields[$key]['y_size'];				
				Filer::createImage($tmp_path, Path::image_files('s_'.$value), $x_size, $y_size );
				Filer::createImage($tmp_path, Path::image_files('b_'.$value));
				//Удаляем картинку из временного хранилища
				Filer::delete($tmp_path);
			}
			
			if (($type == 'file' || $type == 'files') && $value) {
				if ($type == 'file')
				    $values = array($value);
				else
				    $values = explode(',', $value);
				
				foreach ($values as $value) {
			        $tmp_path = Path::temp_files($value);
                    //Копируем файл в нужное место
                    copy($tmp_path, Path::upload($value));
                    //Удаляем файл из из временного хранилища
                    Filer::delete($tmp_path);
				}
			} 
		}
	}
	
	protected function updateFiles() {
		//Удаляем старые картинки
		foreach ($this->properties as $key=>$value) {
            if ($this->fields[$key]['type'] == 'image' && $value) {
            	$row = $this->fastLoadRow();
            	Filer::delete(Path::image_files('b_'.$row[$this->dbf.$key]));
				Filer::delete(Path::image_files('s_'.$row[$this->dbf.$key]));
			}
			if ($this->fields[$key]['type'] == 'file' && $value) {
				$row = $this->fastLoadRow();
                Filer::delete(Path::upload($row[$this->dbf.$key]));
			}
		}
		//Устанавливаем новые
		$this->saveFiles();		
	}
	
	public function deleteFile($field_ext) {
		$row = $this->fastLoadRow();
		$new_value = NULL;
		
		
		$field_ext = explode('#', $field_ext);
		$field = $field_ext[0];
		
		$type = $this->fields[$field]['type'];
		
		$file_name = $row[$this->dbf.$field];
        if ($type == 'image') {
            Filer::delete(Path::image_files('b_'.$file_name));
            Filer::delete(Path::image_files('s_'.$file_name ));
        }
        
		if ($type == 'file') {
            Filer::delete(Path::upload($file_name));
        }
		
		if ($type == 'files') {
			$num = $field_ext[1];
			$old_files = explode(',', $file_name);
			Filer::delete(Path::upload($old_files[$num]));
			unset($old_files[$num]);
			$new_value = implode(',', $old_files);
		}
		
		$this->setFields($field, $new_value)->save(FALSE);
	}
	
    protected function checkNotNull() {
        foreach ($this->fields as $field_name=>$field) {
            if (($this->properties[$field_name] === '' || is_null($this->properties[$field_name])) && $field['not_null'] && !isset($field['default'])) {
                return $this->errorNotNull($field_name);
			}
		}

        return TRUE;
    }
    
    //Убираем префиксы перед названиями полей
    protected function convertFieldsFromDB($fields, $prefix, $db_row) {
        $result['id'] = $db_row[$this->dbf.'id'];
        foreach ($fields as $field_name=>$field) {
            $value = $db_row[$prefix.$field_name];
			if ($field['type'] == 'date') {
				$value = $value['8'].$value['9'].'.'.$value['5'].$value['6'].'.'.$value['0'].$value['1'].$value['2'].$value['3'];				
			}
			$result[$field_name] = $value;
		}
        
        return $result;
    }
    
	protected function convertFieldsToDB($flag = TRUE) {
		$result = array();
		foreach ($this->fields as $key=>$field) {
			$value = $this->properties[$key];
			if ($field['type'] == 'date') {
				$els = explode('.', $value);
				$value = $els[2].'-'.$els[1].'-'.$els[0].' 00:00:00';
			}
			//Файлы должны быть добавлены к уже существующим
			if ($field['type'] == 'files' && $flag) {
				$row = $this->fastLoadRow();
				$old_value = $row[$this->dbf.$key];
				if ($old_value)
				    $value = $old_value.','.$value;  
			}
			
			$result[$key] = $value;
		}
		return $result;
	}
	
    public function delete() {
        DB::query(q::ini($this->tbl, $this->dbf)->delete()->where('id', $this->id)->end);
        $this->is_updated = false;
    }
    
    public function errorNotNull($field_name) {
        $this->errors[] = 'must be not null: '.$field_name;
        return FALSE;
    }
    
    public function errorFieldsNotExist($fields) {
        $this->errors[] = 'fields not exist: '.implode(', ', $fields);
        return FALSE;
    }
    
    public function errorNotExist() {
        $this->errors[] = 'id not found ';
        return FALSE;
    }
    
    public function getErrors() {
        return $this->errors;
    }   
    
    //Возможно получить информацию об объекте, без его явного создания
    public static function getDBInfo($class_name) {
        $obj = new $class_name();
        return $obj->_getDBInfo(); 
    }
    
    //Возвращает информацию о представлении объекта в базе данных
    public function _getDBInfo() {
        return array('table_name'=>$this->tbl, 'field_prefix'=>$this->dbf);
    }
    
    //Позволяет присоединять к объекту дополнительные свойства
    //Позволяет конструировать древовидную структуру из сущностей
    //Например, пользователи у каждого из которых несколько фотографий
    public function join($name, $value, $multiply = false) {
        if (!$multiply)     
            $this->join_properties[$name] = $value;
        else 
            if (isset($this->join_properties[$name]))
                $this->join_properties[$name][] = $value;
            else
                $this->join_properties[$name] = array($value);
    }
    
    public function getJoined($name) {
        return isset($this->join_properties[$name])? $this->join_properties[$name] : FALSE;
    }
	
	//Получаем список полей объекта, которые можно редактировать
	public function getEditableFields() {
		$result = array();
		foreach ($this->fields as $key=>$field) {
			if ($field['editing']) $result[] = $key;
		}
		return $result;
	}
	
	public function getName() {
		return $this->name;
	}
	
	public function getNames() {
		return $this->names;
	}
		
	//Создаёт содержимое формы, позволяющий редактировать объект
	public function getForm($is_delete = FALSE) {
		$this->load();
		$result = '<form id="fs_form">';
		$result .= '<input id="fs_id" type="hidden" name="id" value="'.$this->id.'" />';
		$result .= '<input type="hidden" name="class_name" value="'.$this->class_name.'" />';
		$result .= '<table>';
		foreach ($this->fields as $key=>$field) {
			if (!$field['editing']) continue;
			
			$value = $this->properties[$key];
			
			//Заголовок поля
			$caption = $field['name'];
			
			//Само поле
			$input_element = '';
			
			//Генерируем классы и id (если необходимы) для input'а формы
			$classes = '';            
			$el_id = '';
											
			if ($field['not_null']) $classes .= ' v_required';
			if ($field['type'] == 'date') $classes .= ' datepicker v_date';
							
			if ($field['type'] == 'html') {
				$classes .= ' ckeditor';
				//Уникальный id для ckeditor'а
				$uniq_id = $this->class_name.'_'.$key.'_'.$this->id;
				$el_id = $uniq_id;					
			}
			
			if ($el_id)
			    $el_id = 'id="'.$el_id.'"';
			
			$class = 'class="'.trim($classes).'"';
			
			//В зависимости от типа поля выводим, соответствующий элемент				
			if ($field['type'] == 'string' || $field['type'] == 'date')
			    $input_element = '<input type="text" value="'.$value.'" name="'.$key.'" '.$class.' title="'.$field['name'].'" />';
		    
			if ($field['type'] == 'text' || $field['type'] == 'html')
                $input_element = '<textarea '.$el_id.' name="'.$key.'" '.$class.' title="'.$field['name'].'">'.$value.'</textarea>';
            
			//Этот код необходим для запуска CKEDITOR'а
            if ($field['type'] == 'html')
				$input_element .= '<script type="text/javascript">CKEDITOR.replace(\''.$uniq_id.'\', {filebrowserBrowseUrl: \''.BASE_URL.'libs/FileManager/index.php\'});</script>';

            if ($field['type'] == 'image') {
                if ($value) $input_element .= '<a href="'.url('upload/b_'.$value).'"><img '.img_src('s_'.$value).'align="top" /></a>';
			}
			
            if ($field['type'] == 'file') {
                if ($value) $input_element .= ffile($value);
            }
			
			if ($field['type'] == 'image' || $field['type'] == 'file') {
			    if ($value) $input_element .= '<a id="delete-file_'.$key.'" class="icon cross">&nbsp;</a><br />';
				$input_element .= '<input type="file" name="f_'.$key.'" '.$class.' title="'.$field['name'].'" />';
			}

            if ($field['type'] == 'bool')
			    $input_element = '<input name="'.$key.'" type="checkbox" value="1" '.checked($value).'/>';

            if ($field['type'] == 'files') {
            	if ($value) {
	            	//Старые файлы
					$values = explode(',', $value);
					$i=0;
					foreach ($values as $file) {
					    $input_element .= ffile($file).' <a id="delete-file_'.$key.'#'.$i.'" class="icon cross">&nbsp;</a><br />';
						$i++;	
					}
				}
				//Добавление новых файлов
            	$input_element .= '<div id="template_'.$key.'" class="hidden"><div>
                                   <input type="file" name="f_'.$key.'[]" '.$class.' title="'.$field['name'].'" />
								   </div></div>
								   <input type="file" name="f_'.$key.'[]" '.$class.' title="'.$field['name'].'" /><br />
								   <a id="add-file_'.$key.'" class="action">Ещё файл</a>
								   ';
            }
						
			//Добавляем сгенерированный код к результату
			$asterix = '';
			if ($field['not_null']) $asterix = ' <sup class="not_ok">*</sup>';
			$result .= '<tr><td>'.$caption.$asterix.': </td>';
			$result .= '<td>'.$input_element.'</td></tr>';			 
		}
		$result .= '<tr><td></td><td><input id="fs_sender" type="button" class="button" value="Сохранить" />';
		
		if ($is_delete && $this->id) {
			$result .= '<br /><input id="fs_delete" type="button" class="button" value="Удалить" />';
		}
		
		$result .= '</td>';
		
		$result .= '</table></form>';
		
		return $result;
	}
}

?>